题目:

var a = 1, b = 2;
function fn() {
	var a = 100, b = 200;
	console.log(a + b)
}
fn()

一道很常见的 javascript 题目,输出300

从这道题一出发,捋一遍 EC、ECS、VO、AO、Scope Chain 相关知识点。

# EC

执行上下文(Execution Context),也叫执行环境。

在《javascript高级程序设计》中的定义:

定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

即:EC 决定了当前代码对其他变量或函数的访问权限。

javascript 中会维护一个执行上下文栈 ECS(Execution Context Stack)

  • 栈底是全局上下文(Global Context);
  • 当一个函数执行的时候,会将当前函数的 EC push 进 ECS,函数执行结束 pop 出 ECS。

EC 主要包含三个成员:

  • VO:变量对象
  • Scope Chain:作用域链
  • this

# VO

变量对象(Variable Object)

作用:

  • 保存 EC 的作用域链
  • 保存 EC 的 arguments 和变量

可以理解为数据作用域。

当函数执行的时候,进入函数的 EC,VO 激活为活动对象 AO。当函数开始执行前,VO 进行初始化,包括 arguments 和内部的变量;当函数真正执行代码会将修改 VO 中的 arguments 和内部的变量。

比如上面的题目:

var a = 1, b = 2;
function fn() {
	var a = 100, b = 200;
	console.log(a + b)
}
fn()

函数 fn 在开始执行前,会初始化 AO 的 arguments 和内部的变量。如下:

const fnAO = {
  arguments: {
    length: 0
  },
  a: undefined,
  b: undefined
}

当函数开始执行,修改 AO 的 arguments 和内部的变量。如下:

const fnAO = {
  arguments: {
    length: 0
  },
  a: 100,
  b: 200
}

# Scope Chain

作用域链。

在《javascript高级程序设计》中的解释:

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。

当在 EC 中查找一个变量,会先在当前作用域中进行查找,查找不到就向父级作用域查找,如此循环,直到找到为止;如果找不到,就返回 undefined。

在作用域链的最外级,是全局作用域 Global Context

当函数创建的时候,函数内部有个属性 [[scope]]保存父级 EC 的作用域链;当函数开始执行,先创建函数的 EC,推入 ECS;然后将[[scope]]保存的作用域复制到 EC 中进行保存,接着初始化 VO 的 arguments 和变量,最后将当前 VO 添加到保存的作用域最前端,这样就形成了作用域链。

# 题目流程

结合 EC、ECS、VO、AO、Scope Chain,解析题目。

var a = 1, b = 2;
function fn() {
	var a = 100, b = 200;
	console.log(a + b)
}
fn()

流程为:

  • 函数创建的时候,函数的内部属性[[scope]]会保存外部 EC 的作用域链;
fn.[[scope]] = [
    GlobalContext.VO
]
  • 函数开始执行前,先创建函数的 EC,推入 ECS;
ECStack = [
    fnContext,
    GlobalContext
];
  • [[scope]]保存的作用域链复制到函数执行上下文 EC 中进行保存;
const fnContext = {
	Scope: [ GlobalContext.VO ]
}
  • 初始化函数 AO 的 arguments 和变量;
const fnContext = {
  Scope: [ GlobalContext.VO ],
  fnAO: {
    arguments: {
      length: 0
    },
    a: undefined,
    b: undefined
  }
}
  • 将函数的 AO 添加到作用域的前端;
const fnContext = {
  Scope: [fnAO, GlobalContext.VO],
  fnAO: {
    arguments: {
      length: 0
    },
    a: undefined,
    b: undefined
  }
}
  • 函数执行,修改 VO 对象;
const fnContext = {
  Scope: [fnAO, [[Scope]]],
  fnAO: {
    arguments: {
      length: 0
    },
    a: 100,
    b: 200
  }
}
  • 函数执行结束, EC 推出 ECS。
ECStack = [
    globalContext
];

# 闭包

function fn() {
  let a = 1
  function closureFn() {
    a = a + 1
    return a
  }
  return closureFn
}
const closureFn = fn()
console.log('>>  :', closureFn()); // >>  : 2
  • fn 函数创建的时候,fn 函数的内部属性[[scope]]会保存外部 EC 的作用域链;
fn.[[scope]] = [GlobalContext.VO]
  • fn 函数开始执行前,先创建函数的 EC,推入 ECS;
ECStack = [
    fnContext,
    globalContext
];
  • [[scope]]保存的作用域复制到函数执行上下文 EC 中进行保存;
const fnContext = {
	Scope: [globalContext.VO]
}
  • 初始化 fn 函数 AO 的 arguments 和变量;
const fnContext = {
  Scope: [globalContext.VO],
  fnAO: {
    arguments: {
      length: 0
    }, 
    a: undefined,
    closureFn: undefined
  }
}
  • 将函数的 AO 添加到作用域的前端;
const fnContext = {
  Scope: [AO, globalContext.VO],
  AO: {
    arguments: {
      length: 0
    },
    a: undefined,
    b: undefined
  }
}
  • fn 函数执行,修改 AO 对象,初始化变量和函数,closureFn 函数创建并通过内部属性 [[scope]] 保存父级作用域链。
const fnContext = {
  Scope: [fnAO, globalContext.VO],
  fnAO: {
    arguments: {
      length: 0
    },
    a: 1,
    closureFn.[[scope]]: [AO, globalContext.VO]
  }
}
  • fn 函数执行结束, EC 推出 ECS。
ECStack = [
    globalContext.VO
];
  • closureFn 函数开始执行前,先创建函数的 EC,推入 ECS;
ECStack = [
  	closureFnContext,
    globalContext
];
  • 将 closureFn.[[scope]] 保存的作用域链复制到 closureFn 到 Scope;
const closureFnContext = {
  Scope: [AO, globalContext.VO]
}
  • closureFn 函数执行,初始化 closureFn 函数 VO 的 arguments 和变量;
const closureFnContext = {
  Scope: [AO, globalContext.VO],
  closureFnAO: {
    arguments: {
      length: 0
    }
  }
}
  • 将 closureFnVO 添加到作用域的前端;
const closureFnContext = {
  Scope: [closureFnAO, fnAO, [[Scope]]],
  closureFnAO: {
    arguments: {
      length: 0
    }a:  1 // 通过作用域链 Scope 读取 fnAO 中的变量 a
  }
}
  • closureFn 函数执行结束,closureFnEC 推出 ECS;
ECStack = [
    globalContext
];

虽然父级作用域的代码早已执行结束并推出 ECS,但是由于 closureFn 函数通过作用域链能够访问父级作用域中的数据,所以父级作用域的变量数据会常驻内存。


参考链接:

  • https://github.com/mqyqingfeng/Blog/issues/7
  • https://javascript.info/reference-type